/*
 *    Copyright 2009-2014 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.session;

import org.apache.ibatis.binding.MapperRegistry;
import org.apache.ibatis.builder.CacheRefResolver;
import org.apache.ibatis.builder.ResultMapResolver;
import org.apache.ibatis.builder.annotation.MethodResolver;
import org.apache.ibatis.builder.xml.XMLStatementBuilder;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.decorators.FifoCache;
import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.decorators.SoftCache;
import org.apache.ibatis.cache.decorators.WeakCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.datasource.jndi.JndiDataSourceFactory;
import org.apache.ibatis.datasource.pooled.PooledDataSourceFactory;
import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
import org.apache.ibatis.executor.*;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.loader.ProxyFactory;
import org.apache.ibatis.executor.loader.cglib.CglibProxyFactory;
import org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl;
import org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl;
import org.apache.ibatis.logging.log4j.Log4jImpl;
import org.apache.ibatis.logging.log4j2.Log4j2Impl;
import org.apache.ibatis.logging.nologging.NoLoggingImpl;
import org.apache.ibatis.logging.slf4j.Slf4jImpl;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.InterceptorChain;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.scripting.LanguageDriverRegistry;
import org.apache.ibatis.scripting.defaults.RawLanguageDriver;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.ibatis.transaction.managed.ManagedTransactionFactory;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeAliasRegistry;
import org.apache.ibatis.type.TypeHandlerRegistry;

import java.util.*;

/**
 * @author Clinton Begin
 */
public class Configuration {

    protected Environment environment;

    protected boolean safeRowBoundsEnabled = false;
    protected boolean safeResultHandlerEnabled = true;
    protected boolean mapUnderscoreToCamelCase = false;
    protected boolean aggressiveLazyLoading = true;
    protected boolean multipleResultSetsEnabled = true;
    protected boolean useGeneratedKeys = false;
    protected boolean useColumnLabel = true;
    protected boolean cacheEnabled = true;
    protected boolean callSettersOnNulls = false;
    protected String logPrefix;
    protected Class<? extends Log> logImpl;
    protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
    protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
    protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(
            Arrays.asList(new String[]{
                    "equals", "clone",
                    "hashCode",
                    "toString"}));
    protected Integer defaultStatementTimeout;
    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;

    protected Properties variables = new Properties();
    protected ObjectFactory objectFactory = new DefaultObjectFactory();
    protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
    protected MapperRegistry mapperRegistry = new MapperRegistry(
            this);

    protected boolean lazyLoadingEnabled = false;
    protected ProxyFactory proxyFactory;

    protected String databaseId;
    /**
     * Configuration factory class. Used to create Configuration for loading
     * deserialized unread properties.
     *
     * @see <a
     * href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue
     * 300</a> (google code)
     */
    protected Class<?> configurationFactory;

    protected final InterceptorChain interceptorChain = new InterceptorChain();
    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(
            "Mapped Statements collection");
    protected final Map<String, Cache> caches = new StrictMap<Cache>(
            "Caches collection");
    protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>(
            "Result Maps collection");
    protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>(
            "Parameter Maps collection");
    protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>(
            "Key Generators collection");

    protected final Set<String> loadedResources = new HashSet<String>();
    protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>(
            "XML fragments parsed from previous mappers");

    protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<XMLStatementBuilder>();
    protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<CacheRefResolver>();
    protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<ResultMapResolver>();
    protected final Collection<MethodResolver> incompleteMethods = new LinkedList<MethodResolver>();

    public static final String SHOW_COLUMNS_FROM = "show columns from ";

    public static final String COLUMN_KEY = "column_key";

    public static final String FIELD = "field";

    public static final String LY_TABLE = "ly_table";

    public static final String FINDBYWHERE = "findByWhere";

    public static final String FINDBYPAGE = "findByPage";

    public static final String DELETEBYNAMES = "deleteByNames";

    public static final String DELETEBYATTRIBUTE = "deleteByAttribute";

    public static final String FINDBYNAMES = "findByNames";

    public static final String FINDBYATTRIBUTE = "findByAttribute";

    public static final String ADDENTITY = "addEntity";

    public static final String EDITENTITY = "editEntity";

    public static final String FINDBYFRIST = "findbyFrist";

    public static final String BATCHSAVE = "batchSave";

    /*
     * A map holds cache-ref relationship. The key is the namespace that
     * references a cache bound to another namespace and the value is the
     * namespace which the actual cache is bound to.
     */
    protected final Map<String, String> cacheRefMap = new HashMap<String, String>();

    public Configuration(
            Environment environment) {
        this();
        this.environment = environment;
    }

    public Configuration() {
        typeAliasRegistry
                .registerAlias(
                        "JDBC",
                        JdbcTransactionFactory.class);
        typeAliasRegistry
                .registerAlias(
                        "MANAGED",
                        ManagedTransactionFactory.class);

        typeAliasRegistry
                .registerAlias(
                        "JNDI",
                        JndiDataSourceFactory.class);
        typeAliasRegistry
                .registerAlias(
                        "POOLED",
                        PooledDataSourceFactory.class);
        typeAliasRegistry
                .registerAlias(
                        "UNPOOLED",
                        UnpooledDataSourceFactory.class);

        typeAliasRegistry
                .registerAlias(
                        "PERPETUAL",
                        PerpetualCache.class);
        typeAliasRegistry
                .registerAlias("FIFO",
                        FifoCache.class);
        typeAliasRegistry
                .registerAlias("LRU",
                        LruCache.class);
        typeAliasRegistry
                .registerAlias("SOFT",
                        SoftCache.class);
        typeAliasRegistry
                .registerAlias("WEAK",
                        WeakCache.class);

        typeAliasRegistry
                .registerAlias(
                        "DB_VENDOR",
                        VendorDatabaseIdProvider.class);

        typeAliasRegistry
                .registerAlias(
                        "XML",
                        XMLLanguageDriver.class);
        typeAliasRegistry
                .registerAlias(
                        "RAW",
                        RawLanguageDriver.class);

        typeAliasRegistry
                .registerAlias("SLF4J",
                        Slf4jImpl.class);
        typeAliasRegistry
                .registerAlias(
                        "COMMONS_LOGGING",
                        JakartaCommonsLoggingImpl.class);
        typeAliasRegistry
                .registerAlias("LOG4J",
                        Log4jImpl.class);
        typeAliasRegistry
                .registerAlias(
                        "LOG4J2",
                        Log4j2Impl.class);
        typeAliasRegistry
                .registerAlias(
                        "JDK_LOGGING",
                        Jdk14LoggingImpl.class);
        typeAliasRegistry
                .registerAlias(
                        "STDOUT_LOGGING",
                        StdOutImpl.class);
        typeAliasRegistry
                .registerAlias(
                        "NO_LOGGING",
                        NoLoggingImpl.class);

        typeAliasRegistry
                .registerAlias(
                        "CGLIB",
                        CglibProxyFactory.class);
        typeAliasRegistry
                .registerAlias(
                        "JAVASSIST",
                        JavassistProxyFactory.class);

        languageRegistry
                .setDefaultDriverClass(XMLLanguageDriver.class);
        languageRegistry
                .register(RawLanguageDriver.class);
    }

    public String getLogPrefix() {
        return logPrefix;
    }

    public void setLogPrefix(
            String logPrefix) {
        this.logPrefix = logPrefix;
    }

    public Class<? extends Log> getLogImpl() {
        return logImpl;
    }

    @SuppressWarnings("unchecked")
    public void setLogImpl(
            Class<?> logImpl) {
        if (logImpl != null) {
            this.logImpl = (Class<? extends Log>) logImpl;
            LogFactory
                    .useCustomLogging(this.logImpl);
        }
    }

    public boolean isCallSettersOnNulls() {
        return callSettersOnNulls;
    }

    public void setCallSettersOnNulls(
            boolean callSettersOnNulls) {
        this.callSettersOnNulls = callSettersOnNulls;
    }

    public String getDatabaseId() {
        return databaseId;
    }

    public void setDatabaseId(
            String databaseId) {
        this.databaseId = databaseId;
    }

    public Class<?> getConfigurationFactory() {
        return configurationFactory;
    }

    public void setConfigurationFactory(
            Class<?> configurationFactory) {
        this.configurationFactory = configurationFactory;
    }

    public boolean isSafeResultHandlerEnabled() {
        return safeResultHandlerEnabled;
    }

    public void setSafeResultHandlerEnabled(
            boolean safeResultHandlerEnabled) {
        this.safeResultHandlerEnabled = safeResultHandlerEnabled;
    }

    public boolean isSafeRowBoundsEnabled() {
        return safeRowBoundsEnabled;
    }

    public void setSafeRowBoundsEnabled(
            boolean safeRowBoundsEnabled) {
        this.safeRowBoundsEnabled = safeRowBoundsEnabled;
    }

    public boolean isMapUnderscoreToCamelCase() {
        return mapUnderscoreToCamelCase;
    }

    public void setMapUnderscoreToCamelCase(
            boolean mapUnderscoreToCamelCase) {
        this.mapUnderscoreToCamelCase = mapUnderscoreToCamelCase;
    }

    public void addLoadedResource(
            String resource) {
        loadedResources.add(resource);
    }

    public boolean isResourceLoaded(
            String resource) {
        return loadedResources
                .contains(resource);
    }

    public Environment getEnvironment() {
        return environment;
    }

    public void setEnvironment(
            Environment environment) {
        this.environment = environment;
    }

    public AutoMappingBehavior getAutoMappingBehavior() {
        return autoMappingBehavior;
    }

    public void setAutoMappingBehavior(
            AutoMappingBehavior autoMappingBehavior) {
        this.autoMappingBehavior = autoMappingBehavior;
    }

    public boolean isLazyLoadingEnabled() {
        return lazyLoadingEnabled;
    }

    public void setLazyLoadingEnabled(
            boolean lazyLoadingEnabled) {
        this.lazyLoadingEnabled = lazyLoadingEnabled;
    }

    public ProxyFactory getProxyFactory() {
        if (proxyFactory == null) {
            // makes sure CGLIB is not needed unless explicitly requested
            proxyFactory = new CglibProxyFactory();
        }
        return proxyFactory;
    }

    public void setProxyFactory(
            ProxyFactory proxyFactory) {
        this.proxyFactory = proxyFactory;
    }

    public boolean isAggressiveLazyLoading() {
        return aggressiveLazyLoading;
    }

    public void setAggressiveLazyLoading(
            boolean aggressiveLazyLoading) {
        this.aggressiveLazyLoading = aggressiveLazyLoading;
    }

    public boolean isMultipleResultSetsEnabled() {
        return multipleResultSetsEnabled;
    }

    public void setMultipleResultSetsEnabled(
            boolean multipleResultSetsEnabled) {
        this.multipleResultSetsEnabled = multipleResultSetsEnabled;
    }

    public Set<String> getLazyLoadTriggerMethods() {
        return lazyLoadTriggerMethods;
    }

    public void setLazyLoadTriggerMethods(
            Set<String> lazyLoadTriggerMethods) {
        this.lazyLoadTriggerMethods = lazyLoadTriggerMethods;
    }

    public boolean isUseGeneratedKeys() {
        return useGeneratedKeys;
    }

    public void setUseGeneratedKeys(
            boolean useGeneratedKeys) {
        this.useGeneratedKeys = useGeneratedKeys;
    }

    public ExecutorType getDefaultExecutorType() {
        return defaultExecutorType;
    }

    public void setDefaultExecutorType(
            ExecutorType defaultExecutorType) {
        this.defaultExecutorType = defaultExecutorType;
    }

    public boolean isCacheEnabled() {
        return cacheEnabled;
    }

    public void setCacheEnabled(
            boolean cacheEnabled) {
        this.cacheEnabled = cacheEnabled;
    }

    public Integer getDefaultStatementTimeout() {
        return defaultStatementTimeout;
    }

    public void setDefaultStatementTimeout(
            Integer defaultStatementTimeout) {
        this.defaultStatementTimeout = defaultStatementTimeout;
    }

    public boolean isUseColumnLabel() {
        return useColumnLabel;
    }

    public void setUseColumnLabel(
            boolean useColumnLabel) {
        this.useColumnLabel = useColumnLabel;
    }

    public LocalCacheScope getLocalCacheScope() {
        return localCacheScope;
    }

    public void setLocalCacheScope(
            LocalCacheScope localCacheScope) {
        this.localCacheScope = localCacheScope;
    }

    public JdbcType getJdbcTypeForNull() {
        return jdbcTypeForNull;
    }

    public void setJdbcTypeForNull(
            JdbcType jdbcTypeForNull) {
        this.jdbcTypeForNull = jdbcTypeForNull;
    }

    public Properties getVariables() {
        return variables;
    }

    public void setVariables(
            Properties variables) {
        this.variables = variables;
    }

    public TypeHandlerRegistry getTypeHandlerRegistry() {
        return typeHandlerRegistry;
    }

    public TypeAliasRegistry getTypeAliasRegistry() {
        return typeAliasRegistry;
    }

    /**
     * @since 3.2.2
     */
    public MapperRegistry getMapperRegistry() {
        return mapperRegistry;
    }

    public ObjectFactory getObjectFactory() {
        return objectFactory;
    }

    public void setObjectFactory(
            ObjectFactory objectFactory) {
        this.objectFactory = objectFactory;
    }

    public ObjectWrapperFactory getObjectWrapperFactory() {
        return objectWrapperFactory;
    }

    public void setObjectWrapperFactory(
            ObjectWrapperFactory objectWrapperFactory) {
        this.objectWrapperFactory = objectWrapperFactory;
    }

    /**
     * @since 3.2.2
     */
    public List<Interceptor> getInterceptors() {
        return interceptorChain
                .getInterceptors();
    }

    public LanguageDriverRegistry getLanguageRegistry() {
        return languageRegistry;
    }

    public void setDefaultScriptingLanguage(
            Class<?> driver) {
        if (driver == null) {
            driver = XMLLanguageDriver.class;
        }
        getLanguageRegistry()
                .setDefaultDriverClass(
                        (Class<? extends LanguageDriver>) driver);
    }

    public LanguageDriver getDefaultScriptingLanuageInstance() {
        return languageRegistry
                .getDefaultDriver();
    }

    public MetaObject newMetaObject(
            Object object) {
        return MetaObject.forObject(
                object, objectFactory,
                objectWrapperFactory);
    }

    public ParameterHandler newParameterHandler(
            MappedStatement mappedStatement,
            Object parameterObject,
            BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement
                .getLang()
                .createParameterHandler(
                        mappedStatement,
                        parameterObject,
                        boundSql);
        parameterHandler = (ParameterHandler) interceptorChain
                .pluginAll(parameterHandler);
        return parameterHandler;
    }

    public ResultSetHandler newResultSetHandler(
            Executor executor,
            MappedStatement mappedStatement,
            RowBounds rowBounds,
            ParameterHandler parameterHandler,
            ResultHandler resultHandler,
            BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(
                executor,
                mappedStatement,
                parameterHandler,
                resultHandler,
                boundSql, rowBounds);
        resultSetHandler = (ResultSetHandler) interceptorChain
                .pluginAll(resultSetHandler);
        return resultSetHandler;
    }

    public StatementHandler newStatementHandler(
            Executor executor,
            MappedStatement mappedStatement,
            Object parameterObject,
            RowBounds rowBounds,
            ResultHandler resultHandler,
            BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(
                executor,
                mappedStatement,
                parameterObject,
                rowBounds,
                resultHandler, boundSql);
        statementHandler = (StatementHandler) interceptorChain
                .pluginAll(statementHandler);
        return statementHandler;
    }

    public Executor newExecutor(
            Transaction transaction) {
        return newExecutor(transaction,
                defaultExecutorType);
    }

    public Executor newExecutor(
            Transaction transaction,
            ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType
                : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE
                : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(
                    this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(
                    this, transaction);
        } else {
            executor = new SimpleExecutor(
                    this, transaction);
        }
        if (cacheEnabled) {
            executor = new CachingExecutor(
                    executor);
        }
        executor = (Executor) interceptorChain
                .pluginAll(executor);
        return executor;
    }

    public void addKeyGenerator(
            String id,
            KeyGenerator keyGenerator) {
        keyGenerators.put(id,
                keyGenerator);
    }

    public Collection<String> getKeyGeneratorNames() {
        return keyGenerators.keySet();
    }

    public Collection<KeyGenerator> getKeyGenerators() {
        return keyGenerators.values();
    }

    public KeyGenerator getKeyGenerator(
            String id) {
        return keyGenerators.get(id);
    }

    public boolean hasKeyGenerator(
            String id) {
        return keyGenerators
                .containsKey(id);
    }

    public void addCache(Cache cache) {
        caches.put(cache.getId(), cache);
    }

    public Collection<String> getCacheNames() {
        return caches.keySet();
    }

    public Collection<Cache> getCaches() {
        return caches.values();
    }

    public Cache getCache(String id) {
        return caches.get(id);
    }

    public boolean hasCache(String id) {
        return caches.containsKey(id);
    }

    public void addResultMap(
            ResultMap rm) {
        resultMaps.put(rm.getId(), rm);
        checkLocallyForDiscriminatedNestedResultMaps(rm);
        checkGloballyForDiscriminatedNestedResultMaps(rm);
    }

    public Collection<String> getResultMapNames() {
        return resultMaps.keySet();
    }

    public Collection<ResultMap> getResultMaps() {
        return resultMaps.values();
    }

    public ResultMap getResultMap(
            String id) {
        return resultMaps.get(id);
    }

    public boolean hasResultMap(
            String id) {
        return resultMaps
                .containsKey(id);
    }

    public void addParameterMap(
            ParameterMap pm) {
        parameterMaps.put(pm.getId(),
                pm);
    }

    public Collection<String> getParameterMapNames() {
        return parameterMaps.keySet();
    }

    public Collection<ParameterMap> getParameterMaps() {
        return parameterMaps.values();
    }

    public ParameterMap getParameterMap(
            String id) {
        return parameterMaps.get(id);
    }

    public boolean hasParameterMap(
            String id) {
        return parameterMaps
                .containsKey(id);
    }

    public void addMappedStatement(
            MappedStatement ms) {
        mappedStatements.put(
                ms.getId(), ms);
    }

    public Collection<String> getMappedStatementNames() {
        buildAllStatements();
        return mappedStatements
                .keySet();
    }

    public Collection<MappedStatement> getMappedStatements() {
        buildAllStatements();
        return mappedStatements
                .values();
    }

    public Collection<XMLStatementBuilder> getIncompleteStatements() {
        return incompleteStatements;
    }

    public void addIncompleteStatement(
            XMLStatementBuilder incompleteStatement) {
        incompleteStatements
                .add(incompleteStatement);
    }

    public Collection<CacheRefResolver> getIncompleteCacheRefs() {
        return incompleteCacheRefs;
    }

    public void addIncompleteCacheRef(
            CacheRefResolver incompleteCacheRef) {
        incompleteCacheRefs
                .add(incompleteCacheRef);
    }

    public Collection<ResultMapResolver> getIncompleteResultMaps() {
        return incompleteResultMaps;
    }

    public void addIncompleteResultMap(
            ResultMapResolver resultMapResolver) {
        incompleteResultMaps
                .add(resultMapResolver);
    }

    public void addIncompleteMethod(
            MethodResolver builder) {
        incompleteMethods.add(builder);
    }

    public Collection<MethodResolver> getIncompleteMethods() {
        return incompleteMethods;
    }

    public MappedStatement getMappedStatement(
            String id) {
        return this.getMappedStatement(
                id, true);
    }

    public MappedStatement getMappedStatement(
            String id,
            boolean validateIncompleteStatements) {
        if (validateIncompleteStatements) {
            buildAllStatements();
        }
        return mappedStatements.get(id);
    }

    public Map<String, XNode> getSqlFragments() {
        return sqlFragments;
    }

    public void addInterceptor(
            Interceptor interceptor) {
        interceptorChain
                .addInterceptor(interceptor);
    }

    public void addMappers(
            String packageName,
            Class<?> superType) {
        mapperRegistry.addMappers(
                packageName, superType);
    }

    public void addMappers(
            String packageName) {
        mapperRegistry
                .addMappers(packageName);
    }

    public <T> void addMapper(
            Class<T> type) {
        mapperRegistry.addMapper(type);
    }

    public <T> T getMapper(
            Class<T> type,
            SqlSession sqlSession) {
        return mapperRegistry
                .getMapper(type,
                        sqlSession);
    }

    public boolean hasMapper(
            Class<?> type) {
        return mapperRegistry
                .hasMapper(type);
    }

    public boolean hasStatement(
            String statementName) {
        return hasStatement(
                statementName, true);
    }

    public boolean hasStatement(
            String statementName,
            boolean validateIncompleteStatements) {
        if (validateIncompleteStatements) {
            buildAllStatements();
        }
        return mappedStatements
                .containsKey(statementName);
    }

    public void addCacheRef(
            String namespace,
            String referencedNamespace) {
        cacheRefMap.put(namespace,
                referencedNamespace);
    }

    /*
     * Parses all the unprocessed statement nodes in the cache. It is
     * recommended to call this method once all the mappers are added as it
     * provides fail-fast statement validation.
     */
    protected void buildAllStatements() {
        if (!incompleteResultMaps
                .isEmpty()) {
            synchronized (incompleteResultMaps) {
                // This always throws a BuilderException.
                incompleteResultMaps
                        .iterator()
                        .next()
                        .resolve();
            }
        }
        if (!incompleteCacheRefs
                .isEmpty()) {
            synchronized (incompleteCacheRefs) {
                // This always throws a BuilderException.
                incompleteCacheRefs
                        .iterator()
                        .next()
                        .resolveCacheRef();
            }
        }
        if (!incompleteStatements
                .isEmpty()) {
            synchronized (incompleteStatements) {
                // This always throws a BuilderException.
                incompleteStatements
                        .iterator()
                        .next()
                        .parseStatementNode();
            }
        }
        if (!incompleteMethods
                .isEmpty()) {
            synchronized (incompleteMethods) {
                // This always throws a BuilderException.
                incompleteMethods
                        .iterator()
                        .next()
                        .resolve();
            }
        }
    }

    /*
     * Extracts namespace from fully qualified statement id.
     *
     * @param statementId
     *
     * @return namespace or null when id does not contain period.
     */
    protected String extractNamespace(
            String statementId) {
        int lastPeriod = statementId
                .lastIndexOf('.');
        return lastPeriod > 0 ? statementId
                .substring(0,
                        lastPeriod)
                : null;
    }

    // Slow but a one time cost. A better solution is welcome.
    protected void checkGloballyForDiscriminatedNestedResultMaps(
            ResultMap rm) {
        if (rm.hasNestedResultMaps()) {
            for (Map.Entry<String, ResultMap> entry : resultMaps
                    .entrySet()) {
                Object value = entry
                        .getValue();
                if (value instanceof ResultMap) {
                    ResultMap entryResultMap = (ResultMap) value;
                    if (!entryResultMap
                            .hasNestedResultMaps()
                            && entryResultMap
                            .getDiscriminator() != null) {
                        Collection<String> discriminatedResultMapNames = entryResultMap
                                .getDiscriminator()
                                .getDiscriminatorMap()
                                .values();
                        if (discriminatedResultMapNames
                                .contains(rm
                                        .getId())) {
                            entryResultMap
                                    .forceNestedResultMaps();
                        }
                    }
                }
            }
        }
    }

    // Slow but a one time cost. A better solution is welcome.
    protected void checkLocallyForDiscriminatedNestedResultMaps(
            ResultMap rm) {
        if (!rm.hasNestedResultMaps()
                && rm.getDiscriminator() != null) {
            for (Map.Entry<String, String> entry : rm
                    .getDiscriminator()
                    .getDiscriminatorMap()
                    .entrySet()) {
                String discriminatedResultMapName = entry
                        .getValue();
                if (hasResultMap(discriminatedResultMapName)) {
                    ResultMap discriminatedResultMap = resultMaps
                            .get(discriminatedResultMapName);
                    if (discriminatedResultMap
                            .hasNestedResultMaps()) {
                        rm.forceNestedResultMaps();
                        break;
                    }
                }
            }
        }
    }

    protected static class StrictMap<V>
            extends HashMap<String, V> {

        private static final long serialVersionUID = -4950446264854982944L;
        private String name;

        public StrictMap(String name,
                         int initialCapacity,
                         float loadFactor) {
            super(initialCapacity,
                    loadFactor);
            this.name = name;
        }

        public StrictMap(String name,
                         int initialCapacity) {
            super(initialCapacity);
            this.name = name;
        }

        public StrictMap(String name) {
            super();
            this.name = name;
        }

        public StrictMap(
                String name,
                Map<String, ? extends V> m) {
            super(m);
            this.name = name;
        }

        @SuppressWarnings("unchecked")
        public V put(String key, V value) {
            if (containsKey(key))
                throw new IllegalArgumentException(
                        name
                                + " already contains value for "
                                + key);
            if (key.contains(".")) {
                final String shortKey = getShortName(key);
                if (super.get(shortKey) == null) {
                    super.put(shortKey,
                            value);
                } else {
                    super.put(
                            shortKey,
                            (V) new Ambiguity(
                                    shortKey));
                }
            }
            return super
                    .put(key, value);
        }

        public V get(Object key) {
            V value = super.get(key);
            if (value == null) {
                throw new IllegalArgumentException(
                        name
                                + " does not contain value for "
                                + key);
            }
            if (value instanceof Ambiguity) {
                throw new IllegalArgumentException(
                        ((Ambiguity) value)
                                .getSubject()
                                + " is ambiguous in "
                                + name
                                + " (try using the full name including the namespace, or rename one of the entries)");
            }
            return value;
        }

        private String getShortName(
                String key) {
            final String[] keyparts = key
                    .split("\\.");
            final String shortKey = keyparts[keyparts.length - 1];
            return shortKey;
        }

        protected static class Ambiguity {
            private String subject;

            public Ambiguity(
                    String subject) {
                this.subject = subject;
            }

            public String getSubject() {
                return subject;
            }
        }
    }

}
